# -*- coding: utf-8 -*-
"""
PDF处理公共方法模块: 包含所有PDF处理流程中通用的函数和类
- 切分方法
- 多模态解析模块
"""
import re
import json
from pathlib import Path
from typing import List, Dict, Any

import requests
from openai import OpenAI
from loguru import logger
from langchain_community.document_loaders.base import BaseLoader


PDFMetadata = Dict[str, Any]
ContentChunk = Dict[str, Any]
MarkdownContent = str


class PdfCommonUtils:
    """PDF处理公共工具类"""
    @staticmethod
    def _ping_img(img_url: str) -> Dict[str, Any] | None:
        """
        探测图片信息并返回尺寸与大小。

        Args:
            img_url: 图片的 URL（http/https）

        Returns:
            包含图片信息的字典：{"ok": bool, "content_type": str, "file_size": int, "width": int, "height": int}
            若请求失败或解析失败，返回 None。
        """
        try:
            # 先尝试 HEAD 获取类型与大小
            content_type = ""
            content_length = 0
            try:
                head_resp = requests.head(img_url, timeout=10, allow_redirects=True)
                if head_resp.ok:
                    content_type = (head_resp.headers.get("Content-Type") or "").lower()
                    try:
                        content_length = int(head_resp.headers.get("Content-Length") or 0)
                    except Exception:
                        content_length = 0
            except Exception:
                pass

            # GET 请求以便解析宽高
            resp = requests.get(img_url, timeout=20, stream=True, allow_redirects=True)
            if not resp.ok:
                return None

            if not content_type:
                content_type = (resp.headers.get("Content-Type") or "").lower()
            if content_length == 0:
                try:
                    content_length = int(resp.headers.get("Content-Length") or 0)
                except Exception:
                    content_length = 0

            MAX_BYTES = 20 * 1024 * 1024
            data = bytearray()
            for chunk in resp.iter_content(chunk_size=8192):
                if not chunk:
                    continue
                data.extend(chunk)
                if len(data) > MAX_BYTES:
                    break

            file_size = content_length if content_length > 0 else len(data)
            result: Dict[str, Any] = {
                "ok": True,
                "content_type": content_type or "image/unknown",
                "file_size": int(file_size)
            }
            return result
        except Exception as e:
            logger.debug(f"_ping_img failed for {img_url}: {e}")
            return None

    # 智能预切分：先切碎，再拼接
    @staticmethod
    def smart_pre_split(text_content: str) -> List[str]:
        if len(text_content) <= 7000:
            return [text_content]

        logger.info(f"内容超过7000字符({len(text_content)})，进行智能预切分...")
        lines = text_content.split('\n')

        # 1. 按二级标题切分
        h2_sections = []
        current_section = []
        for line in lines:
            if line.strip().startswith('## ') and current_section:
                # 遇到新的二级标题，保存当前部分
                section_content = '\n'.join(current_section).strip()
                if section_content:
                    h2_sections.append(section_content)
                current_section = [line]
            else:
                current_section.append(line)

        # 添加最后一部分
        if current_section:
            section_content = '\n'.join(current_section).strip()
            if section_content:
                h2_sections.append(section_content)

        # 2. 对超过4000字符的部分按三级标题再切分
        all_small_sections = []
        for section in h2_sections:
            if len(section) > 4000:
                sub_sections = PdfCommonUtils.split_by_h3(section)
                all_small_sections.extend(sub_sections)
            else:
                all_small_sections.append(section)

        # 3. 顺序拼接，超过4000字符就截止
        final_chunks = []
        current_chunk = ""
        for section in all_small_sections:
            if current_chunk and len(current_chunk) + len(section) + 2 > 4000:
                final_chunks.append(current_chunk.strip())
                current_chunk = section
            else:
                current_chunk += "\n\n" + section if current_chunk else section

        if current_chunk.strip():
            final_chunks.append(current_chunk.strip())

        return final_chunks

    # 按三级标题切分内容
    @staticmethod
    def split_by_h3(section_content: str) -> List[str]:
        lines = section_content.split('\n')
        sub_sections = []
        current_sub = []
        for line in lines:
            if line.strip().startswith('### ') and current_sub:
                sub_content = '\n'.join(current_sub).strip()
                if sub_content:
                    sub_sections.append(sub_content)
                current_sub = [line]
            else:
                current_sub.append(line)

        if current_sub:
            sub_content = '\n'.join(current_sub).strip()
            if sub_content:
                sub_sections.append(sub_content)

        return sub_sections

    # 使用LLM对单个文本块进行语义切分
    @staticmethod
    def split_single_chunk_with_llm(text_content: str, filename: str) -> List[Dict]:
        prompt = f"""
        待划分的内容是markdown格式的国家标准条款内容，请将其切分为多个语义完整、有利于检索的 chunks。

        【步骤】
        第一步：纠错。输入的markdown内容存在一定的层级错误，不要完全参考“#”标识的数量，请你务必根据标题数字序号以及上下文内容，进行合理的推测和修复；修复后，再进行内容的切分。
        第二步：切分。严格按照下列切分要求进行切分，首先根据正文内容，判断是按普通要求切分，还是按照特殊要求切分。如果正文中包含大量的“第x条”，就选择按照特殊要求切分。一切切分要求都是为了保证切分后的内容有利于检索。
        
        【普通要求】
        1. 每个 chunk 不少于150个字符，不超过300个字符；
        2. 每个 chunk 都应该完整表达一个概念；如果chunk内容过短，考虑与相邻内容进行合并；
        3. 同级要点尽量在同一个chunk内；比如 “a）、b）、c）” 这种同级要点，应该在同一个chunk内；比如“3.1.1、3.1.2、3.1.3”这种同级要点，应该在同一个chunk内；
        4. chunk前后若有类似：<-- image_00x -->和<-- table_00x -->这种格式的内容，务必与原文内容一起切分，这是对图片和表格标记的保留，非常重要；
        5. 表题、图题、标题、公式、示例等，必须与其相关描述在同一 chunk 内，严格避免出现单独为chunk的情况；
        6. 切分时，最好都是以标题为分界线，不要以句子为分界线，不要以类似“a)”、“(1)”、公式符号为分界线；严禁出现单独的标题、句子为chunk的情况；
        7. 参考预期返回的示例以及不希望返回的示例，返回符合要求的内容；
        8. 最终结果严格返回json；

        【特殊要求】
        1. 以“第×条”为基本切分单位，每条独立作为一个 chunk。 
        2. 每个 chunk 必须完整包含该条的全部内容，不能拆开或截断。
        3. 如果文档前有总则、章节标题等，应将标题附在对应的条目内容前面。 
        4. 遇到条款类的内容，无需严格遵守chunk的150-300字符的要求，就按条目切分即可，不用收到字数限制；
        5. 条款类内容的预期返回示例，如【预期示例二】所示，同一条目之间的内容务必不能断开；
        6. 其余的要求，类似图表标记，请严格遵守【普通要求】中的要求；
        7. 文章开头和结尾会有部分内容，与正文内容无关，请忽略这部分内容；

        【待划分的内容】
        {text_content}

        【预期返回的json格式的示例】
        【预期示例一】
        ```json
        {{
          "chunks": [
            {{
              "id": "1",
              "content": "### 5.3 外观质量 5.3.1 接合器的铸铁件表面应光滑，除锈后上部外露部分应涂红色漆，漆膜色泽应均匀，无龟裂、无明显的划痕和碰伤；接合器的铸铁件内表面应涂防锈漆或采用其他防腐处理。 5.3.2 接合器铸铜件表面应无严重的砂眼、气孔、渣孔、缩松、氧化夹渣、裂纹、冷隔和穿透性缺陷。"
            }},
            {{
              "id": "2",
              "content": "### 4.2 外观和主要部件 4.2.1 产品外观应符合下述要求：a）表面无腐蚀、涂覆层脱落和起泡现象，无明显划伤、裂痕、毛刺等机械损伤；b）紧固部位无松动。4.2.2 指示灯应符合以下要求：表示各种状态的指示灯应用颜色标识。红色表示报警，黄色表示故障，绿色表示正常；所有指示灯应有清楚的功能标注；指示灯点亮时，在其正前方3m处、光照度为1001x~500lx的环境条件下，应清晰可见。4.2.3 在正常工作条件下，距音响器件正前方1m处的声压级（A计权）不应小于70dB。4.2.4 接线端子应符合以下要求：有保护罩；清晰标注功能；强、弱电接线端子分开设置。4.2.5 插座应设有保护接地端子。"
            }},
            ...
          ]
        }}
        ```

        【预期示例二】
        ```json
        {{
          "chunks": [
            {{
              "id": "1",
              "content": "# 第一章 总则 **第一条** 为了保障网络安全，维护网络空间主权和国家安全、社会公共利益，保护公民、法人和其他组织的合法权益，促进经济社会信息化健康发展，制定本法。"
            }},
            {{
              "id": "2",
              "content": "# 第一章 总则 **第二条** 在中华人民共和国境内开展网络空间治理、维护网络空间主权、社会公共利益和公民、法人和其他组织的合法权益，适用本法。"
            }},
            ...
          ]
        }}
        ```

        【不希望返回的示例内容（Badcase）】
        【反面示例一】第二个chunk是一句话，内容短且与上文断开。这种情况正确的做法是应该与上文合并。
        ```json
        {{
          "chunks": [
            {{
              "id": "1",
              "content": "#### 6.15.3 检测方法\n将被测样品佩戴在金属头模上，调整金属头模高度，使燃烧器顶端与面罩最下端的垂直距离为（20±2)mm；然后使金属头模位于燃烧器燃烧区外。\n点燃燃烧器后，调节火焰，使燃烧器顶端的火焰高度达到（40±4)mm，使距离燃烧器顶端（20±2)mm 处的火焰温度达到（800±50）℃。"
            }},
            {{
              "id": "2",
              "content": "启动金属头模运动控制装置，使被测样品经过燃烧区，记录通过火焰上方时面罩材料的燃烧情况。\n应重复检测，检测面罩的所有外表面材料，应使每个部件都通过1次火焰。"
            }},
          ]
        }}
        ```

        【反面示例二】内容过短。这种情况正确的做法是应该与同小节的内容合并。
        ```json
        {{
          "chunks": [
            {{
              "id": "1",
              "content": "#### 6.16.7 检测报告\n每个受试者应按GB/T 23465-2009中第6章和表3的要求，并根据5.15的要求，提供主观评价。检测报告应符合GB/T 23465-2009中第7章的要求。"
            }},
            ...
          ]
        }}
        ```"""
        try:
            api_key = "4130db91-11f6-44cd-b3bb-71584e47c61d"
            llm_id = "ep-20250329162502-f2wnh"
            base_url = "https://ark.cn-beijing.volces.com/api/v3"

            client = OpenAI(api_key=api_key, base_url=base_url)
            response = client.chat.completions.create(
                model=llm_id,
                messages=[
                    {"role": "system", "content": "你是一个专业的文档分析专家，专门负责企业制度文档的语义切分。"},
                    {"role": "user", "content": prompt},
                ],
                stream=False,
                response_format={'type': 'json_object'},
                extra_body={"enable_thinking": False},
            )

            content = response.choices[0].message.content
            raw = content.strip()
            # 去除代码块围栏
            raw = raw.replace("```json", "").replace("```", "").strip()

            def _try_parse(text: str):
                try:
                    return json.loads(text)
                except Exception:
                    return None

            data = _try_parse(raw)
            # 若为双重编码的字符串，再解一次
            if isinstance(data, str):
                inner = _try_parse(data)
                data = inner if inner is not None else data

            # 若仍未解析成对象，尝试截取其中的 JSON 片段 { "chunks": ... }
            if not isinstance(data, dict):
                m = re.search(r"\{\s*\"chunks\"\s*:\s*\[.*\]\s*\}", raw, flags=re.DOTALL)
                if m:
                    data = _try_parse(m.group(0)) or {}
                else:
                    raise ValueError("无法解析为包含 chunks 的 JSON 对象")

            chunks = data.get("chunks", [])
            if not isinstance(chunks, list):
                raise ValueError("返回 JSON 中 'chunks' 不是列表")

            # 兜底：缺少 id 时自动生成；必须包含 content
            normalized = []
            auto_id = 1
            for ck in chunks:
                if not isinstance(ck, dict):
                    continue
                content_text = ck.get("content")
                if not isinstance(content_text, str) or not content_text.strip():
                    continue
                cid = ck.get("id")
                if not cid:
                    cid = str(auto_id)
                    auto_id += 1
                normalized.append({"id": cid, "content": content_text})

            return normalized

        except Exception as e:
            logger.warning(f"LLM JSON 解析失败, 使用备用文本切分方法: Error: {e}")
            return PdfCommonUtils.fallback_text_split(text_content)

    # 简单的备用文本切分方法
    @staticmethod
    def fallback_text_split(text_content: str) -> List[Dict]:
        chunks = []
        lines = text_content.split('\n')
        current_chunk = []
        current_length = 0
        chunk_id = 1

        CHUNK_SIZE = 200
        i = 0

        while i < len(lines):
            line = lines[i]
            current_chunk.append(line)
            current_length += len(line) + 1

            if current_length >= CHUNK_SIZE:
                found_title = False
                for j in range(i + 1, min(i + 21, len(lines))):
                    if lines[j].strip().startswith('#'):
                        for k in range(i + 1, j):
                            current_chunk.append(lines[k])
                            current_length += len(lines[k]) + 1
                        found_title = True
                        i = j - 1
                        break

                # 保存当前chunk
                if len(current_chunk) > 0:
                    chunk_content = '\n'.join(current_chunk).strip()
                    if chunk_content:
                        chunks.append({
                            "id": f"text_chunk_{chunk_id:03d}",
                            "content": chunk_content
                        })
                        chunk_id += 1

                current_chunk = []
                current_length = 0

            i += 1

        if current_chunk:
            chunk_content = '\n'.join(current_chunk).strip()
            if chunk_content:
                chunks.append({
                    "id": f"text_chunk_{chunk_id:03d}",
                    "content": chunk_content
                })

        return chunks

    # 清洗掉文件名之前的内容
    @staticmethod
    def clean_file_name_content(content):
        """基于extract_file_name函数，清洗掉第二个文件名之前的内容"""
        lines = content.split('\n')

        # 收集前30行中以"#"开头的行，同时记录行号
        title_lines = []
        for i, line in enumerate(lines[:30]):
            stripped_line = line.strip()
            # 匹配以一个或多个"#"开头的行（后面可以有空格也可以没有）
            if re.match(r'^#+', stripped_line):
                # 提取标题内容，去掉所有的"#"和前后空格
                title_content = re.sub(r'^#+\s*', '', stripped_line).strip()
                if title_content:  # 确保提取出的内容不为空
                    title_lines.append((title_content, i))  # 保存内容和行号

        # 查找相同的标题内容
        for i in range(len(title_lines)):
            for j in range(i + 1, len(title_lines)):
                if title_lines[i][0] == title_lines[j][0] and title_lines[i][0]:
                    # 找到第二个文件名的行号
                    second_filename_line = title_lines[j][1]
                    print(f"找到重复标题: '{title_lines[i][0]}', 第二次出现在第{second_filename_line + 1}行")
                    # 从第二个文件名所在行开始保留内容
                    cleaned_content = '\n'.join(lines[second_filename_line:])
                    print(f"内容截取：从第{second_filename_line + 1}行开始，删除了前{second_filename_line}行内容")
                    return cleaned_content

        return content

    # 使用LLM对文本进行语义切分
    @staticmethod
    def split_text_with_llm(text_content: str, filename: str) -> List[Dict]:
        pre_chunks = PdfCommonUtils.smart_pre_split(text_content)
        all_chunks = []
        for i, chunk_content in enumerate(pre_chunks):
            # chunk_result = PdfCommonUtils.split_single_chunk_with_llm(chunk_content, f"{filename}_part{i + 1}")
            chunk_result = PdfCommonUtils.fallback_text_split(chunk_content)
            all_chunks.extend(chunk_result)
        return all_chunks

    # 解析多模态内容：表格、图片（含表内图片）、文本，并保留表/图ID标记。
    @staticmethod
    def parse_multimodal_content(file_path: Path) -> List[Dict]:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()

        chunks: List[Dict] = []
        chunk_id: int = 1
        table_images: List[str] = []  # 收集表格中的图片URL

        # 0. 清洗掉文件名之前的内容（0905新增）
        content = PdfCommonUtils.clean_file_name_content(content)

        # 0) 为表格与独立图片添加标记ID，避免表内图片重复打标
        content = PdfCommonUtils.add_table_and_image_ids_to_content(content)

        # 1) 找到所有表格（含位置），并生成表格与表内图片的chunk
        table_pattern = r'<table\s+border="1"\s*>.*?</table>'
        tables = []
        for match in re.finditer(table_pattern, content, re.DOTALL | re.MULTILINE):
            table_html = match.group(0)
            table_start = match.start()

            # 计算表格开始行号
            table_start_line = content[:table_start].count('\n')
            tables.append((table_html, table_start_line))

        for i, (table_html, table_start_line) in enumerate(tables):
            table_with_title = PdfCommonUtils.extract_table_with_title(content, table_html, table_start_line)
            table_id = f"table_{i+1:03d}"

            chunks.append({
                'type': 'table',
                'id': f"chunk_{chunk_id:03d}",
                'table_id': table_id,
                'content': table_with_title.strip()
            })
            chunk_id += 1

            # 表格中的图片链接
            table_img_urls = re.findall(r'<img src="([^"]+)"', table_html)
            md_table_img_urls = re.findall(r'!\[.*?\]\((https?://[^)]+)\)', table_html)
            table_images.extend(table_img_urls)
            table_images.extend(md_table_img_urls)

            for img_url in table_img_urls + md_table_img_urls:
                chunks.append({
                    'type': 'image_in_table',
                    'id': f"chunk_{chunk_id:03d}",
                    'content': f"表格中的图片: {img_url}",
                    'image_url': img_url
                })
                chunk_id += 1

        # 2) 提取独立图片（非表内），并使用下一行的 image_id 注释进行关联
        #    同时生成独立图片的 chunk（对齐 test.py 流程）
        standalone_images_with_ids: List[tuple[str, str]] = []
        lines = content.split('\n')
        filtered_lines = []

        for i, line in enumerate(lines):
            # 检查是否是独立图片行
            matches = re.findall(r'!\[.*?\]\((https://[^)]+)\)', line)
            
            if matches:
                # 检查这些图片是否在表格中
                is_standalone = False
                for img_url in matches:
                    if img_url not in table_images:
                        is_standalone = True
                        # 查找下一行的 image_id 标记
                        image_id = None
                        if i + 1 < len(lines):
                            next_line = lines[i + 1]
                            id_match = re.search(r'<-- (image_\d+) -->', next_line)
                            if id_match:
                                image_id = id_match.group(1)
                        if image_id:
                            standalone_images_with_ids.append((img_url, image_id))
                
                # 如果是独立图片，跳过URL行，但保留ID标记行
                if is_standalone:
                    continue  # 跳过图片URL行
        
            filtered_lines.append(line)

        clean_content = '\n'.join(filtered_lines)

        # 4) 为独立图片创建 chunk，包含 image_id 与 URL 信息
        for i, (img_url, image_id) in enumerate(standalone_images_with_ids):
            chunks.append({
                'type': 'image',
                'id': f"chunk_{chunk_id:03d}",
                'image_id': image_id,
                'content': f"图片链接: {img_url}",
                'image_url': img_url
            })
            chunk_id += 1

        # 移除其他HTML注释，但保留图片ID和表格ID注释
        clean_content = re.sub(r'<!--(?!\s*(?:image|table)_\d+\s*-->).*?-->', '', clean_content, flags=re.DOTALL)
        clean_content = '\n'.join([line.strip() for line in clean_content.split('\n') if line.strip()])
    
        # debug_file = Path(OUTPUT_DIR) / f"{Path(file_path).stem}_cleaned.md"
        # with open(debug_file, 'w', encoding='utf-8') as f:
        #     f.write(clean_content)
        # print(f"已保存清理后的md到: {debug_file}")

        # 5. 使用LLM对剩余文本进行语义切分（包含表格和图片标记）
        if len(clean_content) > 100:  # 只有足够长的文本才进行LLM切分
            text_chunks = PdfCommonUtils.split_text_with_llm(clean_content, file_path.name)
            
            # 转换为统一格式并分配ID（兜底过滤：丢弃小于30字符的文本chunk）
            MIN_TEXT_CHUNK_LEN = 30
            for text_chunk in text_chunks:
                content_text = (text_chunk.get('content') or '').strip()
                if len(content_text) < MIN_TEXT_CHUNK_LEN:
                    continue
                chunks.append({
                    'type': 'text',
                    'id': f"chunk_{chunk_id:03d}",
                    'content': content_text
                })
                chunk_id += 1

        # 5) 将表格中的图片 URL 替换为 image_id 属性
        chunks = PdfCommonUtils.map_table_images_to_ids(chunks)

        return chunks

    # 为md内容中的表格和独立图片添加标记ID
    @staticmethod
    def add_table_and_image_ids_to_content(content: str) -> str:
        # 首先找到所有表格，避免在表格中的图片添加id
        table_pattern = r'<table\s+border="1"\s*>.*?</table>'
        tables = re.findall(table_pattern, content, re.DOTALL | re.MULTILINE)

        # 提取表格中的图片URL，用于后续过滤
        table_image_urls = []
        for table_html in tables:
            table_image_urls.extend(re.findall(r'<img src="([^"]+)"', table_html))
            table_image_urls.extend(re.findall(r'!\[.*?\]\((https?://[^)]+)\)', table_html))

        lines = content.split('\n')
        modified_lines = []
        image_counter = table_counter = 1

        i = 0
        while i < len(lines):
            line = lines[i]

            # 检查当前行是否包含表格开始标签
            if '<table' in line and 'border="1"' in line:
                # 向上查找3行寻找表题
                table_title = ""
                for j in range(max(0, i-3), i):
                    prev_line = lines[j].strip()
                    # 匹配表题格式，如 "表5 试验程序"、"Table 1"等
                    if re.search(r'表\s*\d+|Table\s*\d+', prev_line, re.IGNORECASE):
                        table_title = prev_line
                        break

                if table_title:
                    logger.info(f"找到表题: {table_title}")

                # 添加表格标记
                table_id = f"table_{table_counter:03d}"
                modified_lines.append(f"<-- {table_id} -->")
                table_counter += 1
                # print(f"为表格添加ID: {table_id}")

            modified_lines.append(line)

            # 检查当前行是否包含独立图片
            image_pattern = r'!\[.*?\]\((https://[^)]+)\)'
            matches = re.findall(image_pattern, line)

            if matches:
                # 检查这些图片是否在表格中
                for img_url in matches:
                    if img_url not in table_image_urls:
                        # 这是独立图片，添加image_id
                        image_id = f"image_{image_counter:03d}"
                        modified_lines.append(f"<-- {image_id} -->")
                        image_counter += 1

            i += 1

        return '\n'.join(modified_lines)

    # 将表格中的图片URL替换为image_id属性，并为表内图片分配连续ID。
    @staticmethod
    def map_table_images_to_ids(chunks: List[Dict]) -> List[Dict]:
        # 1) 找到已存在的独立图片的最大 image_id 序号
        max_image_id = 0
        for chunk in chunks:
            if chunk.get("type") == "image" and chunk.get("image_id"):
                image_id = chunk.get("image_id")
                if "_" in image_id:
                    try:
                        idx = int(image_id.split("_")[1])
                        max_image_id = max(max_image_id, idx)
                    except Exception:
                        continue

        # 2) 为 image_in_table 块分配新的 image_id，并建立 URL -> image_id 映射
        url_to_image_id: Dict[str, str] = {}
        current_id = max_image_id + 1

        for chunk in chunks:
            if chunk.get("type") == "image_in_table":
                content = chunk.get("content", "")
                if ": " in content:
                    url = content.split(": ", 1)[1]
                    image_id = f"image_{current_id:03d}"
                    chunk["image_id"] = image_id
                    url_to_image_id[url] = image_id
                    current_id += 1

        # 3) 替换表格 HTML 中的 <img src="http://..."> 为 <img src="img">
        def replace_img(match):
            img_url = match.group(1)
            if img_url in url_to_image_id:
                return f'<img src="{url_to_image_id[img_url]}">'
            return match.group(0)

        for chunk in chunks:
            if chunk.get("type") == "table":
                html_content = chunk.get("content", "")
                chunk["content"] = re.sub(r'<img src="([^"]+)"[^>]*>', replace_img, html_content)

        return chunks

    # 提取表格内容及其上方的表题
    @staticmethod
    def extract_table_with_title(content, table_html, table_start_line):
        lines = content.split('\n')

        # 向上查找3行寻找表题
        table_title = ""
        for j in range(max(0, table_start_line-3), table_start_line):
            if j < len(lines):
                prev_line = lines[j].strip()
                # 匹配表题格式，如 "表5 试验程序"、"Table 1"等
                if re.search(r'表\s*\d+|Table\s*\d+', prev_line, re.IGNORECASE):
                    table_title = prev_line
                    break

        # 组合表题和表格内容
        if table_title:
            return f"{table_title}\n\n{table_html}"
        else:
            return table_html

    # 标准化chunks结果，拆分主文本与附件并统计类型
    @staticmethod
    def analyze_result(result: Dict) -> PDFMetadata:
        chunks = result.get("chunks", [])
        main_text: List[Dict] = []
        attachments: List[Dict] = []
        chunk_stats: Dict[str, int] = {}

        for chunk in chunks:
            chunk_type = chunk.get('type', 'unknown')
            if chunk_type in ("text", "table"):
                main_text.append(chunk)
            if chunk_type in ("image", "image_in_table", "table"):
                attachments.append(chunk)
            chunk_stats[chunk_type] = chunk_stats.get(chunk_type, 0) + 1

        return {
            "success": True,
            "filename": result.get("filename", ""),
            "gb_number": result.get("gb_number", ""),
            "scope": result.get("scope", ""),
            "category": result.get("category", ""),
            "main_text": main_text,
            "attachments": attachments,
            "chunk_stats": chunk_stats
        }


class BasePdfLoader(BaseLoader):
    """通用PDF Loader基类：统一load流程，子类仅需实现PDF->MD与清洗逻辑，以及可选的元数据提取。"""

    def __init__(self, file_path: str, delete_old_file: bool = False):
        self.file_path = Path(file_path)
        self.base_dir = self.file_path.parent
        self.delete_old_file = delete_old_file

    def _pdf_to_markdown(self, json_path: Path, md_path: Path) -> None:
        raise NotImplementedError

    def _clean_markdown(self, in_path: Path, out_path: Path) -> None:
        raise NotImplementedError

    def _extract_metadata(self, cleaned_md_text: str, md_filename: str, **kwargs) -> Dict[str, Any]:
        return {"gb_number": "", "scope": "", "filename": "", "category": "xx"}
    
    def _parse_content(self, cleaned_md_path: Path, output_path: Path, metadata: Dict[str, Any]) -> Dict:
        chunks = PdfCommonUtils.parse_multimodal_content(cleaned_md_path)
        result = {
            **metadata,
            "total_chunks": len(chunks),
            "chunks": chunks
        }
        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(result, f, ensure_ascii=False, indent=2)
        return result

    def load(self, delete_old_file: bool = None) -> PDFMetadata:
        raise NotImplementedError
